using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Net.NetworkInformation;
using System.Security.Cryptography;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Threading;
using System.Text.RegularExpressions;
using System.Reflection;
using System.IO.Compression;

namespace net.notdot.Attercop
{
    public class MessageEventArgs<T> : EventArgs where T : Message
    {
        public readonly T Message;

        public MessageEventArgs(T m)
        {
            Message = m;
        }
    }

    public class PeerEventArgs : EventArgs
    {
        public readonly Peer Peer;

        public PeerEventArgs(Peer p)
        {
            Peer = p;
        }
    }

    /// <summary>
    /// An instance of an Attercop client.
    /// </summary>
    public class ClientInstance { 
        #region Fields and Delegates

        public readonly NetworkSettings NetworkSettings;
        public readonly UserSettings UserSettings;
        public readonly Peer LocalPeer;

        public delegate IList<SearchResult> SearchDelegate(SearchMessage m);

        public readonly IFileHandler FileHandler;
        //TODO: Make readonly again, and make ServerWindow instantiate the ClientInstance.
        public SearchDelegate SearchHandler;

        #endregion Fields

        #region Events

        private void RaiseMessageEvent<T>(EventHandler<MessageEventArgs<T>> handler, T message) where T : Message
        {
            if (handler != null)
                handler.Invoke(this, new MessageEventArgs<T>(message));
        }
        
        public event EventHandler<MessageEventArgs<ChatMessage>> ChatEvent;

        internal void RaiseChatEvent(ChatMessage m)
        {
            RaiseMessageEvent<ChatMessage>(ChatEvent, m);
        }

        public event EventHandler<PeerEventArgs> PeerJoin;

        internal void RaisePeerJoin(Peer p)
        {
            if (PeerJoin != null)
            {
                PeerJoin.Invoke(this, new PeerEventArgs(p));
            }
        }

        public event EventHandler<PeerEventArgs> PeerQuit;

        internal void RaisePeerQuit(Peer p)
        {
            if (PeerQuit != null)
            {
                PeerQuit.Invoke(this, new PeerEventArgs(p));
            }
        }

        #endregion

        #region Internal Members

        internal Dictionary<IPEndPoint, Peer> Peers = new Dictionary<IPEndPoint, Peer>();
        internal QueueDictionary<int, PingMessage> pings = new QueueDictionary<int, PingMessage>();
        internal Dictionary<int, Query> searches = new Dictionary<int, Query>();

        #endregion

        #region Private Members 

        private QueueDictionary<string, Message> recentMessages = new QueueDictionary<string, Message>();
        private static Dictionary<string, ConstructorInfo> messageHandlers = new Dictionary<string, ConstructorInfo>();
        private DateTime LastHello = DateTime.MinValue;

        private Socket unicastSocket;
        private Socket multicastSocket;
        private Thread messageThread;
        private Timer cleanupTimer;

        #endregion

        #region Constructors and Destructors

        public ClientInstance(NetworkSettings networkSettings, UserSettings userSettings, IFileHandler fileHandler) {
            LocalPeer = userSettings.Peer;
            NetworkSettings = networkSettings;
            UserSettings = userSettings;
            FileHandler = fileHandler;

            unicastSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            unicastSocket.Bind(new IPEndPoint(IPAddress.Any, UserSettings.Port));

            multicastSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            //multicastSocket.MulticastLoopback = true;
            try {
                multicastSocket.Bind(new IPEndPoint(IPAddress.Any, NetworkSettings.NetworkAddress.Port));
                multicastSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(NetworkSettings.NetworkAddress.Address, IPAddress.Any));
            }
            catch (Exception) { }

            messageThread = new Thread(new ThreadStart(MessageThreadMain));
            messageThread.IsBackground = true;
            messageThread.Start();

            //TODO: Add cleanup thread that cleans the recentMessages and pings collections.
            //and sends regular 'hello' messages.
            cleanupTimer = new Timer(new TimerCallback(CleanupProcess), null, 1000, 1000);
        }

        ~ClientInstance() {
            messageThread.Abort();
        }

        #endregion

        #region Public Methods

        private const int FRAGMENTATION_THRESHOLD = 1400;
        private const int FRAGMENT_SIZE = 800;

        public void SendMessage(Message m)
        {
            MemoryStream ms = new MemoryStream();
            DeflateStream dms = new DeflateStream(ms, CompressionMode.Compress, true);
            SerializeMessage(dms, m);
            dms.Close();

            if (ms.Position > FRAGMENTATION_THRESHOLD)
            {
                //Split the message, if we can
                Message[] pieces = m.Split((int)Math.Ceiling(ms.Length / (double)FRAGMENT_SIZE));
                if (pieces != null)
                {
                    foreach (Message piece in pieces)
                        SendMessage(piece);
                    return;
                }
            }

            Socket s = unicastSocket;
            s.SendTo(ms.GetBuffer(), (int) ms.Position, SocketFlags.None, m.Addressing.To);
        }

        public Query Search(string searchString, long minSize, long maxSize, string mimeType, bool? isDir, EventHandler<MessageEventArgs<SearchResultMessage>> resultHandler)
        {
            Query ret = new Query(resultHandler);
            searches.Add(ret.SearchNo, ret);
            SendMessage(new SearchMessage(new MessageAddressing(LocalPeer.Address, NetworkSettings.NetworkAddress), ret.SearchNo, searchString, minSize, maxSize, mimeType, isDir));

            return ret;
        }

        public void CancelSearch(Query q)
        {
            searches.Remove(q.SearchNo);
        }

        public static void AddMessageHandler<T>() where T : Message
        {
            Type t = typeof(T);
            if (messageHandlers.ContainsKey(t.FullName))
                throw new InvalidOperationException("A message handler is already registered for that message type");

            ConstructorInfo constructor = typeof(T).GetConstructor(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance, null, new Type[] { typeof(MessageReader) }, null);
            if(constructor == null)
                throw new InvalidOperationException("Message class must have a constructor accepting a MessageReader");

            messageHandlers.Add(t.FullName, constructor);
        }

        #endregion

        #region Public Properties

        public IEnumerator<KeyValuePair<IPEndPoint, Peer>> OnlinePeers {
            get {
                return new FilteredEnumerator<KeyValuePair<IPEndPoint, Peer>>(Peers.GetEnumerator(), new FilteredEnumerator<KeyValuePair<IPEndPoint, Peer>>.FilterDelegate(OnlinePeerFilter));
            }
        }

        #endregion

        #region Private and Protected Methods
        
        #region Thread and Event Handlers

        /// <summary>
        /// Implements the message receiving thread.
        /// </summary>
        private void MessageThreadMain() {
            EndPoint remoteep = new IPEndPoint(IPAddress.Any, 0);
            byte[] messageData = new byte[65536];
            MessageAddressing ma;
            UdpMessage m;
            ArrayList mysocks = new ArrayList();

            while (true) {

                mysocks.Clear();
                mysocks.Add(unicastSocket);
                mysocks.Add(multicastSocket);

                //Wait until one or more sockets are ready to be read from
                Socket.Select(mysocks, null, null, 10000000);

                //Read messages from the ready sockets
                foreach (Object o in mysocks) {
                    Socket sock = (Socket)o;
                    sock.ReceiveFrom(messageData, ref remoteep);

                    //Determine the from and to endpoints
                    if (sock == multicastSocket)
                    {
                        ma = new MessageAddressing((IPEndPoint)remoteep, NetworkSettings.NetworkAddress);
                    }
                    else
                    {
                        ma = new MessageAddressing((IPEndPoint)remoteep, (IPEndPoint)sock.LocalEndPoint);
                    }

                    //Deserialize and verify the message
                    m = deserializeMessage(new DeflateStream(new MemoryStream(messageData, false), CompressionMode.Decompress, false), ma) as UdpMessage;

                    //If the message failed verification, ignore it.
                    if (m == null) continue;

                    //Handle the message
                    handleMessage(m);
                }
            }
        }

        private void CleanupProcess(object context)
        {
            DateTime now = DateTime.Now;

            //Clean up the recentMessages collection
            while (recentMessages.Count > 0 && NetworkSettings.WindowDuration.CompareTo(now.Subtract(recentMessages.Peek().ReceivedAt)) < 0)
            {
                Message m = recentMessages.Dequeue();
                m.Sender.MinSequenceNumber = Math.Max(m.Sender.MinSequenceNumber, m.SequenceNumber);
            }

            //Clean up the pings collection
            lock (pings)
            {
                while (pings.Count > 0 && NetworkSettings.PingTimeout.CompareTo(now.Subtract(pings.Peek().ReceivedAt)) < 0)
                    pings.Dequeue();
            }

            //Send hello messages to the network
            if (NetworkSettings.HelloInterval.CompareTo(now.Subtract(LastHello)) < 0)
            {
                SendMessage(new HelloMessage(new MessageAddressing(LocalPeer.Address, NetworkSettings.NetworkAddress), LocalPeer));
                LastHello = DateTime.Now;
            }

            //Check for peers we haven't heard from in a while
            List<IPEndPoint> oldPeers = new List<IPEndPoint>();
            foreach (KeyValuePair<IPEndPoint, Peer> c in Peers)
            {
                if (c.Value.Offline && NetworkSettings.PeerRemove.CompareTo(now.Subtract(c.Value.LastSeen)) < 0) {
                    oldPeers.Add(c.Key);
                }
                else if (!c.Value.Offline && NetworkSettings.PeerTimeout.CompareTo(now.Subtract(c.Value.LastSeen)) < 0)
                {
                    c.Value.m_Offline = true;
                    RaisePeerQuit(c.Value);
                }
            }

            foreach (IPEndPoint c in oldPeers)
                Peers.Remove(c);
        }

        #endregion

        #region Serialization/Deserialization

        private Message deserializeMessage(Stream st, MessageAddressing addressing) {
            Stream s = st;

            //If we're using network encryption, add a decryptor to the stream
            SymmetricAlgorithm sa = NetworkSettings.GetEncryptionAlgorithm();
            if (sa != null) {
                //Read in the IV
                byte[] iv = new byte[sa.BlockSize];
                s.Read(iv, 0, sa.BlockSize);

                //Wrap the stream with a decryptor
                s = new CryptoStream(s, sa.CreateDecryptor(), CryptoStreamMode.Read);
            }

            //Add a hash algorithm instance so we can calculate the hash of the message
            //for signature verification and other purposes.
            HashAlgorithm hash = HashAlgorithm.Create(NetworkSettings.MessageHashAlgorithm);
            CryptoStream s2 = new CryptoStream(s, hash, CryptoStreamMode.Read);

            //Deserialize the message
            /*BinaryFormatter formatter = new BinaryFormatter();
            Message msg = formatter.Deserialize(s2) as Message;*/
            Message msg = null;
            MessageReader reader = new MessageReader(s2);
            string typeName = reader.ReadString();
            reader.BeginRegion();
            if (messageHandlers.ContainsKey(typeName))
            {
                //If we're familiar with the message, deserialize it
                msg = (Message) messageHandlers[typeName].Invoke(new object[] { reader });
            }
            reader.EndRegion();

            if (msg == null) return null;

            msg.m_Addressing = addressing;

            //Get the user's identifying endpoint
            IPEndPoint userep = msg.Addressing.From;

            //Get the peer entry (if any) for this user
            Peer p = null;
            if(Peers.ContainsKey(userep))
                p = Peers[userep];

            //Finalize and retrieve the hash
            s2.FlushFinalBlock();
            byte[] hashdata = hash.Hash;

            //If we don't know them, stop here.
            if (p == null) {
                msg.m_PeerState = PeerState.Unknown;
                return msg;
            }

            msg.m_Sender = p;

            //Read and verify the signature            
            byte[] sig = new byte[p.PublicKey.KeySize / 8];
            s.Read(sig, 0, sig.Length);

            reader.Close();

            if (!p.PublicKey.VerifyHash(hash.Hash, NetworkSettings.MessageHashAlgorithm, sig)) {
                //The message did not verify. Discard it.
                return null;
            }

            //Verify we haven't received the message recently, and if we haven't, add it to our dictionary
            //of recent messages.
            if (recentMessages.ContainsKey(Convert.ToBase64String(hash.Hash))) return null;
            recentMessages.Add(Convert.ToBase64String(hash.Hash), msg);

            if (p.Introduced) {
                //We've been introduced to them. Verify the message isn't a replay
                msg.m_PeerState = PeerState.Introduced;

                if (msg.SequenceNumber <= p.MinSequenceNumber) 
                    return null;

                //Update the last seen value
                p.m_LastSeen = DateTime.Now;
            }
            else {
                //We know this peer, but haven't been introduced to them
                msg.m_PeerState = PeerState.Known;
            }

            return msg;
        }

        private void SerializeMessage(Stream st, Message m) {
            Stream s = st;

            //If we're using network encryption, output the IV then add an encryptor to the stream
            SymmetricAlgorithm sa = NetworkSettings.GetEncryptionAlgorithm();
            if (sa != null) {
                //Write the IV
                s.Write(sa.IV, 0, sa.IV.Length);

                //Wrap the stream with an encryptor
                s = new CryptoStream(s, sa.CreateEncryptor(), CryptoStreamMode.Write);
            }

            //Add a hash algorithm instance so we can calculate the hash of the message
            //for signature generation and other purposes.
            HashAlgorithm hash = HashAlgorithm.Create(NetworkSettings.MessageHashAlgorithm);

            //Serialize the message to the stream
            /*BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(s2, m);*/
            MemoryStream ms = new MemoryStream();
            MessageWriter writer = new MessageWriter(ms);
            writer.Write(m.GetType().FullName);
            writer.BeginRegion();
            m.Serialize(writer);
            writer.EndRegion();

            //Read the message from the MemoryStream and write it to the output stream
            byte[] messageData = ms.GetBuffer();
            s.Write(messageData, 0, (int) ms.Position);

            //Get the hash
            byte[] hashdata = hash.ComputeHash(messageData, 0, (int) ms.Position);

            //Close the writer and memory stream
            writer.Close();

            //Generate and write the signature.
            byte[] sig = UserSettings.RSA.SignHash(hashdata, NetworkSettings.MessageHashAlgorithm);
            s.Write(sig, 0, sig.Length);

            writer.Close();
        }

        #endregion

        protected void handleMessage(UdpMessage m) {
            Peer p = ((Message)m).Sender;
            if(p != null) p.m_LastSeen = DateTime.Now;

            m.Handle(this);
        }


        private bool OnlinePeerFilter(KeyValuePair<IPEndPoint, Peer> val) {
            return val.Value.Introduced && !val.Value.Offline;
        }

        #endregion
    }
}
